﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks;

using Kalman;
using Newtonsoft.Json.Linq;
using Kalman.Extensions;
using RestSharp;
using System.Security.Cryptography;
using System.IO;
using Kalman.Security;
using System.Text;
using NLog;
using QuDao;
using System.Web;

namespace Wechat.MiniApp
{
    /// <summary>
    /// 微信小程序服务端API调用封装
    /// </summary>
    public sealed partial class WxAppClient : SingletonBase<WxAppClient>
    {
        readonly ILogger log;
        readonly AppSettings appSettings;

        public WxAppClient() : base()
        {
            appSettings = AppSettings.Load();
            log = LogManager.GetLogger("WxApp");
        }

        /// <summary>
        /// 获取微信小程序会话数据（session_key和openid）
        /// </summary>
        /// <param name="appid">微信小程序AppID</param>
        /// <param name="code">前端通过调用wx.login方法获取到的登录凭证</param>
        /// <returns></returns>
        public WxaSessionModel GetWxaSession(string appid, string code)
        {
            /* 
             * 微信小程序一键登录流程
             * 1.小程序端调用wx.login()方法获取临时登录凭证js_code
             * 2.小程序调用服务端接口GetWxaSession来获取会话数据（session_key和openid）
             * 3.小程序处理获取手机号按钮事件获取加密数据后调服务端解密接口来获取手机号
             * 4.小程序将获取到的用户手机号和用户信息（wx.getUserInfo）作为请求数据调服务端登录接口Login
             * 5.登录成功后服务端返回用户登录凭证及相关用户信息
             */

            var app = WxAppBO.Instance.GetItem(appid);
            if (app == null) throw new AppException($"无效的应用：{appid}");

            //开发者服务器使用登录凭证 code 获取 session_key 和 openid。其中 session_key 是对用户数据进行加密签名的密钥。为了自身应用安全，session_key 不应该在网络上传输。
            string url = string.Format("https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code",
                app.WxAppID, app.WxAppKey, code);
            var client = new RestClient();
            var request = new RestRequest(url, Method.GET);

            /*  
             * 通过 wx.login() 获取到用户登录态之后，需要维护登录态。开发者要注意不应该直接把 session_key、openid 等字段作为用户的标识或者 session 的标识，
             * 而应该自己派发一个 session 登录态（请参考登录时序图）。对于开发者自己生成的 session，应该保证其安全性且不应该设置较长的过期时间。
             * session 派发到小程序客户端之后，可将其存储在 storage ，用于后续通信使用。
             * 
             * 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序，session_key 有效期越长，
             * 开发者在 session_key 失效时，可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效，从而避免小程序反复执行登录流程，
             * 具体请参考：https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
             */

            //{"session_key":"fQ2jb+BrK3Rm+1\/WyCGGFw==","expires_in":7200,"openid":"owvP60EonmuLsyw6IxWlSFO1OxEI"}
            var response = client.Execute(request);
            if (!response.IsSuccessful)
            {
                throw new AppException($"微信小程序授权登录失败：{response.ErrorMessage}");
            }

            var jo = JObject.Parse(response.Content);

            if (jo.ContainsKey("errcode"))
            {
                var errorCode = jo["errcode"].ToObject<int>();
                if (errorCode != 0)
                {
                    throw new AppException($"微信小程序授权登录失败，错误代码：{errorCode}");
                }
            }

            return new WxaSessionModel
            {
                OpenID = jo["openid"].ToString(),
                SessionKey = jo["session_key"].ToString()
            };
        }

        /// <summary>
        /// 刷新微信小程序服务端API访问令牌
        /// </summary>
        public WxApp RefreshAccessToken(WxApp app)
        {
            /* 获取access_token接口地址：https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
                     * access_token是公众号的全局唯一接口调用凭据，公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时，需定时刷新，重复获取将导致上次获取的access_token失效。

                       公众平台的API调用所需的access_token的使用及生成方式说明：
                        1、为了保密appsecrect，第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器，不应该各自去刷新，否则会造成access_token覆盖而影响业务；
                        2、目前access_token的有效期通过返回的expire_in来传达，目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中，中控服务器对外输出的依然是老access_token，此时公众平台后台会保证在刷新短时间内，新老access_token都可用，这保证了第三方业务的平滑过渡；
                        3、access_token的有效时间可能会在未来有调整，所以中控服务器不仅需要内部定时主动刷新，还需要提供被动刷新access_token的接口，这样便于业务服务器在API调用获知access_token已超时的情况下，可以触发access_token的刷新流程。
                 
                     * 公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在微信公众平台官网-开发页中获得（需要已经成为开发者，且帐号没有异常状态）。注意调用所有微信接口时均需使用https协议。如果第三方不使用中控服务器，而是选择各个业务逻辑点各自去刷新access_token，那么就可能会产生冲突，导致服务不稳定。
                     */
            var url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", app.WxAppID, app.WxAppKey);
            var client = new RestClient();
            var request = new RestRequest(url, Method.GET);

            // 成功：{"access_token":"ACCESS_TOKEN","expires_in":7200}，失败：{"errcode":40013,"errmsg":"invalid appid"}
            var response = client.Execute(request);
            if (!response.IsSuccessful)
            {
                throw new AppException($"获取微信小程序服务端API访问令牌失败：{response.ErrorMessage}");
            }

            var jo = JObject.Parse(response.Content);
            if (jo.ContainsKey("errcode"))
            {
                var errorCode = jo["errcode"].ToObject<int>();
                var errorMsg = jo["errmsg"].ToString();
                if (errorCode != 0)
                {
                    throw new AppException($"获取微信小程序服务端API访问令牌失败，错误信息：{errorCode}-{errorMsg}");
                }
            }

            int expires_in = jo["expires_in"].ToObject<int>();
            app.AccessToken = jo["access_token"].ToString();
            app.TokenExpiredTime = DateTime.Now.AddSeconds(expires_in);

            WxAppBO.Instance.UpdateToken(app.WxAppID, app.AccessToken, app.TokenExpiredTime);
            return app;
        }

        /// <summary>
        /// 获取微信小程序服务端API访问令牌（为防止冲突，正式环境直接从微信服务器获取，测试环境从正式环境获取）
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <returns></returns>
        public string GetAccessToken(string appid)
        {
            var app = WxAppBO.Instance.GetItem(appid, true);
            if (app == null) return string.Empty;
            if (app.AccessToken.IsNullOrEmpty() || DateTime.Now.AddMinutes(10) > app.TokenExpiredTime)
            {
                app = RefreshAccessToken(app);//令牌为空或提前十分钟刷新令牌
            }
            return app.AccessToken;
        }

        #region 小程序码接口
        /// <summary>
        /// 获取微信小程序码（接口A，数量限制10万）
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="model"></param>
        /// <returns></returns>
        public byte[] GetWxaCode(string appid, WxaCodeModel model)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html
            var accessToken = GetAccessToken(appid);
            var param = new { path = model.Path, width = model.Width, auto_color = model.AutoColor, line_color = model.LineColor };

            try
            {
                var url = $"https://api.weixin.qq.com/wxa/getwxacode?access_token={accessToken}";
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var data = client.DownloadData(request);

                if (data.Length < 1000)
                {
                    var msg = Encoding.UTF8.GetString(data);
                    throw new Exception(msg);
                }

                return data;
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("access_token is invalid"))
                {
                    WxAppBO.Instance.UpdateToken(appid, string.Empty, DateTime.Now.AddDays(-1));
                }
                throw new AppException($"获取微信小程序码失败（接口A）：{ex.Message}");
            }
        }

        /// <summary>
        /// 获取微信小程序码（接口B，数量不受限）
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="model"></param>
        /// <returns></returns>
        public byte[] GetUnlimitedWxaCode(string appid, WxaCodeUnlimitedModel model)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
            var accessToken = GetAccessToken(appid);
            var param = new { scene = model.Scene, page = model.Page, width = model.Width, auto_color = model.AutoColor, line_color = model.LineColor };

            try
            {
                var url = $"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={accessToken}";
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var data = client.DownloadData(request);

                if (data.Length < 1000)
                {
                    var msg = Encoding.UTF8.GetString(data);
                    throw new Exception(msg);
                }

                return data;
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("access_token is invalid"))
                {
                    WxAppBO.Instance.UpdateToken(appid, string.Empty, DateTime.Now.AddDays(-1));
                }
                throw new AppException($"获取微信小程序码失败（接口B）：{ex.Message}");
            }
        }

        /// <summary>
        /// 获取微信小程序二维码（接口C，数量限制10万）
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="path">扫码进入的小程序页面路径，最大长度 128 字节，不能为空；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。</param>
        /// <param name="width">二维码的宽度，单位 px。最小 280px，最大 1280px</param>
        /// <returns></returns>
        public byte[] GetWxaQrCode(string appid, string path, int width = 430)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.createQRCode.html
            var accessToken = GetAccessToken(appid);
            var param = new { path = path, width = width };

            try
            {
                var url = $"https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token={accessToken}";
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var data = client.DownloadData(request);

                if (data.Length < 1000)
                {
                    var msg = Encoding.UTF8.GetString(data);
                    throw new Exception(msg);
                }

                return data;
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("access_token is invalid"))
                {
                    WxAppBO.Instance.UpdateToken(appid, string.Empty, DateTime.Now.AddDays(-1));
                }
                throw new AppException($"获取微信小程序二维码码失败（接口C）：{ex.Message}");
            }
        }
        #endregion

        #region URL Scheme,URL Link及Short Link

        /// <summary>
        /// 查询URL Scheme码
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="scheme">小程序scheme码</param>
        /// <returns></returns>
        public string QueryUrlScheme(string appid, string scheme)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/queryScheme.html
            var accessToken = GetAccessToken(appid);
            var url = $"https://api.weixin.qq.com/wxa/queryscheme?access_token=ACCESS_TOKEN={accessToken}";
            var param = new { scheme };

            try
            {
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var response = client.Execute(request);

                return response.Content;
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("access_token is invalid"))
                {
                    WxAppBO.Instance.UpdateToken(appid, string.Empty, DateTime.Now.AddDays(-1));
                }
                throw new AppException($"查询URL Scheme【{scheme}】失败：{ex.Message}");
            }
        }

        /// <summary>
        /// 获取URL Scheme码
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="scheme">小程序scheme码</param>
        /// <returns></returns>
        public string GetUrlScheme(string appid, WxaUrlSchemeModel model)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/generateScheme.html
            var accessToken = GetAccessToken(appid);
            var url = $"https://api.weixin.qq.com/wxa/generatescheme?access_token=ACCESS_TOKEN={accessToken}";

            try
            {
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(model);
                var response = client.Execute(request);

                return response.Content;
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("access_token is invalid"))
                {
                    WxAppBO.Instance.UpdateToken(appid, string.Empty, DateTime.Now.AddDays(-1));
                }
                throw new AppException($"获取URL Scheme失败：{ex.Message}");
            }
        }

        //URL Link:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/generateUrlLink.html

        //Short Link:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/short-link/generateShortLink.html

        #endregion


        #region OCR识别接口
        /// <summary>
        /// 身份证 OCR 识别（一天100次的免费额度）
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="imgUrl"></param>
        /// <returns></returns>
        public string IDCardOCR(string appid, string imgUrl)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.idcard.html
            var accessToken = GetAccessToken(appid);
            //var param = new { path = path, width = width };

            try
            {
                //{"errcode":0,"errmsg":"ok","type":"Front","name":"金正恩","id":"0810198301082010","addr":"朝鲜平圳市中区主席办公室","gender":"男","nationality":"朝鲜","birth":"1983-1-8","card_property":5}
                var url = $"https://api.weixin.qq.com/cv/ocr/idcard?type=MODE&img_url={HttpUtility.UrlEncode(imgUrl)}&access_token={accessToken}";
                var client = new RestClient();
                //var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var request = new RestRequest(url, Method.POST);
                var response = client.Execute(request);
                return response.Content;
            }
            catch (Exception ex)
            {
                throw new AppException($"获取微信小程序二维码码失败（接口C）：{ex.Message}");
            }
        }

        #endregion

        /// <summary>
        /// 发送小程序订阅消息（需要小程序端用户授权）
        /// </summary>
        /// <param name="appid">微信小程序应用ID</param>
        /// <param name="model"></param>
        /// <returns></returns>
        public string SendSubMsg(string appid, WxaMsgSendModel model)
        {
            //接口文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
            var accessToken = GetAccessToken(appid);
            var param = new { touser = model.ToUser, template_id = model.TemplateID, page = model.Page, data = model.Data, miniprogram_state = model.State };

            try
            {
                var url = $"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={accessToken}";
                var client = new RestClient();
                var request = new RestRequest(url, Method.POST).AddJsonBody(param);
                var response = client.Execute(request);
                return response.Content;
            }
            catch (Exception ex)
            {
                throw new AppException($"发送小程序订阅消息失败：{ex.Message}");
            }
        }

        #region Common Method
        private JObject Execute(string url, JObject body)
        {
            var method = body == null ? Method.GET : Method.POST;

            var client = new RestClient();
            var request = new RestRequest(url, method)
                .AddHeader("Content-Type", "application/json")
                .AddHeader("Accept", "application/json")
                //.AddJsonBody(body);
                .AddParameter("application/json", body.ToString(), ParameterType.RequestBody);

            try
            {
                log.Info($"{method} {url} \n {body}");
                var response = client.Execute(request);
                if (!response.IsSuccessful)
                {
                    throw new AppException(response.Content);
                }

                log.Error(response.Content);
                return JObject.Parse(response.Content);
            }
            catch (Exception ex)
            {
                log.Error(ex);
                throw;
            }
        }

        /// <summary>
        /// 微信小程序的用户敏感数据解密算法
        /// </summary>
        /// <param name="encryptedData">待解密的用户敏感数据</param>
        /// <param name="key">微信小程序用户登录返回的session_key</param>
        /// <param name="iv"></param>
        /// <returns></returns>
        public string AESDecrypt(string encryptedData, string key, string iv)
        {
            if (string.IsNullOrEmpty(encryptedData)) return string.Empty;

            var aes = new RijndaelManaged
            {
                KeySize = 128,//设置 cipher 格式 AES-128-CBC    
                Padding = PaddingMode.PKCS7,
                Mode = CipherMode.CBC,
                Key = Convert.FromBase64String(key),
                IV = Convert.FromBase64String(iv)
            };

            byte[] data = Convert.FromBase64String(encryptedData);
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV); //解密

            using MemoryStream msDecrypt = new MemoryStream(data);
            using CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
            using StreamReader srDecrypt = new StreamReader(csDecrypt);

            var result = srDecrypt.ReadToEnd();
            return result;
        }

        #endregion

    }
}
